SPORE.$Import('core.object');
SPORE.$Import('core.class.extra');
SPORE.$Import('core.element.base');

/*
---

name: Request

description: Powerful all purpose Request Class. Uses XMLHTTPRequest.

license: MIT-style license.

requires: [Object, Element, Chain, Events, Options, Browser]

provides: Request

...
*/

SPORE.register('core.request.base',function($ns){
	
	var empty = function(){},
		progressSupport = ('onprogress' in new Browser.Request);
	
	var Request = this.Request = new Class({
	
		Implements: [Chain, Events, Options],
	
		options: {/*
			onRequest: function(){},
			onLoadstart: function(event, xhr){},
			onProgress: function(event, xhr){},
			onComplete: function(){},
			onCancel: function(){},
			onSuccess: function(responseText, responseXML){},
			onFailure: function(xhr){},
			onException: function(headerName, value){},
			onTimeout: function(){},
			user: '',
			password: '',*/
			url: '',
			data: '',
			headers: {
				'X-Requested-With': 'XMLHttpRequest',
				'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
			},
			async: true,
			format: false,
			method: 'post',
			link: 'ignore',
			isSuccess: null,
			emulation: true,
			urlEncoded: true,
			encoding: 'utf-8',
			evalScripts: false,
			evalResponse: false,
			timeout: 0,
			noCache: false
		},
	
		initialize: function(options){
			this.xhr = new Browser.Request();
			this.setOptions(options);
			this.headers = this.options.headers;
		},
	
		onStateChange: function(){
			var xhr = this.xhr;
			if (xhr.readyState != 4 || !this.running) return;
			this.running = false;
			this.status = 0;
			Function.attempt(function(){
				var status = xhr.status;
				this.status = (status == 1223) ? 204 : status;
			}.bind(this));
			xhr.onreadystatechange = empty;
			if (progressSupport) xhr.onprogress = xhr.onloadstart = empty;
			clearTimeout(this.timer);
			
			this.response = {text: this.xhr.responseText || '', xml: this.xhr.responseXML};
			if (this.options.isSuccess.call(this, this.status))
				this.success(this.response.text, this.response.xml);
			else
				this.failure();
		},
	
		isSuccess: function(){
			var status = this.status;
			return (status >= 200 && status < 300);
		},
	
		isRunning: function(){
			return !!this.running;
		},
	
		processScripts: function(text){
			if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) return Browser.exec(text);
			return text.stripScripts(this.options.evalScripts);
		},
	
		success: function(text, xml){
			this.onSuccess(this.processScripts(text), xml);
		},
	
		onSuccess: function(){
			this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
		},
	
		failure: function(){
			this.onFailure();
		},
	
		onFailure: function(){
			this.fireEvent('complete').fireEvent('failure', this.xhr);
		},
		
		loadstart: function(event){
			this.fireEvent('loadstart', [event, this.xhr]);
		},
		
		progress: function(event){
			this.fireEvent('progress', [event, this.xhr]);
		},
		
		timeout: function(){
			this.fireEvent('timeout', this.xhr);
		},
	
		setHeader: function(name, value){
			this.headers[name] = value;
			return this;
		},
	
		getHeader: function(name){
			return Function.attempt(function(){
				return this.xhr.getResponseHeader(name);
			}.bind(this));
		},
	
		check: function(){
			if (!this.running) return true;
			switch (this.options.link){
				case 'cancel': this.cancel(); return true;
				case 'chain': this.chain(this.caller.pass(arguments, this)); return false;
			}
			return false;
		},
		
		send: function(options){
			if (!this.check(options)) return this;
	
			this.options.isSuccess = this.options.isSuccess || this.isSuccess;
			this.running = true;
	
			var type = typeOf(options);
			if (type == 'string' || type == 'element') options = {data: options};
	
			var old = this.options;
			options = Object.append({data: old.data, url: old.url, method: old.method}, options);
			var data = options.data, url = String(options.url), method = options.method.toLowerCase();
	
			switch (typeOf(data)){
				case 'element': data = document.id(data).toQueryString(); break;
				case 'object': case 'hash': data = Object.toQueryString(data);
			}
	
			if (this.options.format){
				var format = 'format=' + this.options.format;
				data = (data) ? format + '&' + data : format;
			}
	
			if (this.options.emulation && !['get', 'post'].contains(method)){
				var _method = '_method=' + method;
				data = (data) ? _method + '&' + data : _method;
				method = 'post';
			}
	
			if (this.options.urlEncoded && ['post', 'put'].contains(method)){
				var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
				this.headers['Content-type'] = 'application/x-www-form-urlencoded' + encoding;
			}
	
			if (!url) url = document.location.pathname;
			
			var trimPosition = url.lastIndexOf('/');
			if (trimPosition > -1 && (trimPosition = url.indexOf('#')) > -1) url = url.substr(0, trimPosition);
	
			if (this.options.noCache)
				url += (url.contains('?') ? '&' : '?') + String.uniqueID();
	
			if (data && method == 'get'){
				url += (url.contains('?') ? '&' : '?') + data;
				data = null;
			}
	
			var xhr = this.xhr;
			if (progressSupport){
				xhr.onloadstart = this.loadstart.bind(this);
				xhr.onprogress = this.progress.bind(this);
			}
	
			xhr.open(method.toUpperCase(), url, this.options.async, this.options.user, this.options.password);
			if (this.options.user && 'withCredentials' in xhr) xhr.withCredentials = true;
			
			xhr.onreadystatechange = this.onStateChange.bind(this);
	
			Object.each(this.headers, function(value, key){
				try {
					xhr.setRequestHeader(key, value);
				} catch (e){
					this.fireEvent('exception', [key, value]);
				}
			}, this);
	
			this.fireEvent('request');
			xhr.send(data);
			if (!this.options.async) this.onStateChange();
			if (this.options.timeout) this.timer = this.timeout.delay(this.options.timeout, this);
			return this;
		},
	
		cancel: function(){
			if (!this.running) return this;
			this.running = false;
			var xhr = this.xhr;
			xhr.abort();
			clearTimeout(this.timer);
			xhr.onreadystatechange = empty;
			if (progressSupport) xhr.onprogress = xhr.onloadstart = empty;
			this.xhr = new Browser.Request();
			this.fireEvent('cancel');
			return this;
		}
	
	});
	
	var methods = {};
	['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function(method){
		methods[method] = function(data){
			var object = {
				method: method
			};
			if (data != null) object.data = data;
			return this.send(object);
		};
	});
	
	Request.implement(methods);
	
	Element.Properties.send = {
	
		set: function(options){
			var send = this.get('send').cancel();
			send.setOptions(options);
			return this;
		},
	
		get: function(){
			var send = this.retrieve('send');
			if (!send){
				send = new Request({
					data: this, link: 'cancel', method: this.get('method') || 'post', url: this.get('action')
				});
				this.store('send', send);
			}
			return send;
		}
	
	};
	
	Element.implement({
	
		send: function(url){
			var sender = this.get('send');
			sender.send({data: this, url: url || sender.options.url});
			return this;
		}
	
	});

});